/*
 * Author: Daniel Stephen, daniel.macc.stephen@gmail.com
 * Date: September, 2016
 * This code was created as part of my masters thesis at Oregon State University.
 * Please contact me before using any code from this application.
 * 
 * The following code is for an interactive flow map depicting US county-to-
 * county migration. It uses a new algorithmic method of arranging flow curves
 * in a manner that adheres to cartographic principles for origin-destination
 * flow maps that improve readability and aesthetics. 
 * 
 * The following Javascript libaries are used in the creation of this map:
 * 
 *    D3.js
 *    TopoJSON.js
 *    jQuery
 *    Bootstrap
 *    simple_statistics.js
 * 
 * The code is written in a similar pattern to the one described here:
 *    http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html
 */

/**
 * The map component module handles all the drawing of map features. It uses
 * D3.js to set up the coordinate system, draw svg features, and handle
 * user interactions with map features.
 */


import Flox from "./Flox"
var d3 = require("d3")
var $ = require("jquery")
var topojson = require("topojson")
var ss = require("./simple_statistics")


var MapComponent_d3 = function () {
    "use strict";

    var svg, // The svg div that contains all map features
        model_copy, // The model containing the flows to be drawn.
        windowWidth = $(window).width(),
        windowHeight = $(window).height(),

        // A d3 scale object. Takes a value, outputs an rgb string
        populationDensityColor,

        mapScale = 1, // TODO obsolete? Used for debug mostly


        // Set the projection for the map 等加载了地图后再进行设置
        projection = null,
        // projection = projection_mercator,
        //先定义一个投影
        projection = d3.geo.mercator()
            .center([110.75, 0.264638163393125])
            .translate([-windowWidth / 8, -windowHeight / 8])
            .scale(65000),

        // generates SVG path data strings using the projection 等加载了地图后再进行设置
        path = null,


        path = d3.geo.path().projection(projection),

        // A d3 zoom object. These are the initial settings when the app
        // first start, which is zoomed in very far.
        zoom = d3.behavior.zoom()
            .translate([windowWidth / 2, windowHeight / 2]) // center of map
            .scale(1) // at the maximum zoom
            .scaleExtent([0.02, 20])// change these numbers to be able to zoom in or out further.
            .on("zoom", zoomed), // call zoomed on zoom

        tooltipOffset = {x: 8, y: -38}, // FIXME y is not used

        // Create a tooltip div
        tooltip = d3.select("body").append("div")
            .attr("class", "floxTooltip")
            .style("display", "none"),

        defaultClasses = 7, // The default number of classes in the choropleth
        stateStrokeWidth = 2,
        stateHoverStrokeWidth = 3,
        countyStrokeWidth = 8,
        countyHoverStrokeWidth = 16,

        // All the colors used on the map should be in here
        colors = {
            backgroundFill: [235, 235, 242],

            minPopDensityFill: [246, 211, 101],
            maxPopDensityFill: [253, 160, 133],

            minFlowStroke: [25,148,117, 1],
            maxFlowStroke: [25,148,117, 1],

            necklaceNodeFill: [96, 191, 191],
            necklaceNodeStroke: [44, 99, 99],

            polygonStroke: [0, 0, 0],

            flowHoverStroke: [255, 255, 255],
            necklaceNodeHoverFill: [189, 229, 187],
            polygonHoverStroke: [0, 0, 0],

            unselectedStateFill: [230, 230, 230],
            unselectedStateStroke: [215, 215, 215],
            unselectedStateHoverStroke: [190, 190, 190],
        },

        // Lookup table for tooltips of the type of administrative unit
        // being hovered over.
        lookupLSAD = {
            "03": "City and Borough",
            "04": "Borough",
            "05": "Census Area",
            "06": "County",
            "07": "District",
            "12": "Municipality",
            "13": "Municipio",
            "15": "Parish",
            "25": "City"
        },

        tooltipEnabled = true, // Flag for turning off tooltips
        starting = true, // True only when the map is initializing.
        my = {};

    function showTooltip() {
        if (tooltipEnabled) {
            tooltip.style("display", "inline");
        }
    }

    function hideTooltip() {
        tooltip.style("display", "none");
    }

    // 将rgb数组转换为字符，注意支持rgba
    function rgbArrayToString(rgb) {
        if (rgb[3]) {
            return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
        }
        return "rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")";
    }

    //将数字按照三个来进行分割 主要是为了显示效果
    function numberWithCommas(x) {
        var parts = x.toString().split(".");
        parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        return parts.join(".");
    }

    //获得地图的中心
    function getCenters(features) {
        var longitudeMin = 100000;
        var latitudeMin = 100000;
        var longitudeMax = 0;
        var latitudeMax = 0;
        features.forEach(function (e) {
            var a = d3.geo.bounds(e);
            if (a[0][0] < longitudeMin) {
                longitudeMin = a[0][0];
            }
            if (a[0][1] < latitudeMin) {
                latitudeMin = a[0][1];
            }
            if (a[1][0] > longitudeMax) {
                longitudeMax = a[1][0];
            }
            if (a[1][1] > latitudeMax) {
                latitudeMax = a[1][1];
            }
        });

        var a = (longitudeMax + longitudeMin) / 2;
        var b = (latitudeMax + latitudeMin) / 2;

        return [a, b];
    }

    //设置地图的大小
    function getZoomScale(features, width, height) {
        var longitudeMin = 100000;
        var latitudeMin = 100000;
        var longitudeMax = 0;
        var latitudeMax = 0;
        features.forEach(function (e) {
            var a = d3.geo.bounds(e);

            if (a[0][0] < longitudeMin) {
                longitudeMin = a[0][0];
            }
            if (a[0][1] < latitudeMin) {
                latitudeMin = a[0][1];
            }
            if (a[1][0] > longitudeMax) {
                longitudeMax = a[1][0];
            }
            if (a[1][1] > latitudeMax) {
                latitudeMax = a[1][1];
            }
        });

        var a = longitudeMax - longitudeMin;
        var b = latitudeMax - latitudeMin;

        return Math.min(width / a, height / b);
    }

    function buildFlowTooltipText(d) {
        var moverText = "<span class='tooltipValue'>" + numberWithCommas(d.getValue()) + "</span>",
            typeText,
            toFromText;

        if (Flox.getFlowType() === "total") {
            // it's a total flow
            typeText = " <div class='tooltipTypeHolder'><br/>O+D</div><br/>";
            // But sometimes it's directional and needs to be treated differently.
            toFromText = "<div class='tooltipToFromText'>" + d.getStartPt().name
                + " <span class='tooltipTypeHolder'>  到  </span> "
                + d.getEndPt().name + ": " + numberWithCommas(d.AtoB)
                + "<br/>" + d.getEndPt().name
                + " <span class='tooltipTypeHolder'>  到  </span> "
                + d.getStartPt().name + ": " + numberWithCommas(d.BtoA)
                + "</div>";
        } else if (Flox.getFlowType() === "net") {
            // it's a net flow
            typeText = " <div class='tooltipTypeHolder'><br/>O-D</div><br/>";
            toFromText = "<div class='tooltipToFromText'><span class='tooltipTypeHolder'>源 </span> "
                + d.getStartPt().name + "<br/>" + "<span class='tooltipTypeHolder'>去 </span> "
                + d.getEndPt().name + "</div>";
        } else {
            typeText = " <div class='tooltipTypeHolder'><br/></div><br/>";
            toFromText = "<div class='tooltipToFromText'><span class='tooltipTypeHolder'>源 </span> "
                + d.getStartPt().name + "<br/>" + "<span class='tooltipTypeHolder''>去 </span> "
                + d.getEndPt().name + "</div>";
        }
        return moverText + typeText + toFromText;
    }

    function buildAreaTooltipText(d, model) {
        var node,
            incomingFlow = 0,
            outgoingFlow = 0,
            areaName,
            inValue,
            outValue,
            inOutText,
            inState,
            inStateText = "",
            toText = "",
            fromText = "";

        if (d.properties.JD_NAME) {
            // it's a county
            node = model.findNodeByID(Number(d.properties.OBJECTID));
            // node = d.properties;
        } else {
            // it's a state
            node = model.findNodeByID((Number(d.properties.OBJECTID)));
            // node = d.properties
        }

        if (Flox.isCountyMode() && !d.properties.JD_NAME) {
            // We're seeing counties, and flow values of states is just for the
            // selected state. 注意注释了的
            inState = Flox.getSelectedStateName();
            inStateText = " <div class='tooltipAreaName'>" + inState + "</div>";
            toText = "去往";
            fromText = "来自";
        }

        if (node.totalOutgoingFlow && node.totalIncomingFlow) {
            outgoingFlow = node.totalOutgoingFlow;
            incomingFlow = node.totalIncomingFlow;
        }
        // var name = node.XZQ_NAME != undefined ? node.XZQ_NAME : node.JD_NAME
        debugger
        var name = node.name;
        areaName = "<div class='tooltipAreaName'>" + name + "</div>";
        inValue = "<span class='tooltipValue'>" + numberWithCommas(incomingFlow)
            + "</span> <div class='tooltipTypeHolder'>进<br/>" + fromText
            + "</div>" + inStateText + "<br/>";
        outValue = "<span class='tooltipValue'>" + numberWithCommas(outgoingFlow)
            + "</span> <div class='tooltipTypeHolder'>出<br/>" + toText
            + "</div>" + inStateText + "<br/>";

        inOutText = "<div class='tooltipInOutText'>" + inValue +
            outValue + "</div>";
        return areaName + inOutText;
    }

    function buildNecklaceNodeTooltipText(d, inState) {
        var nodeName = d.name,
            incomingFlow = d.totalIncomingFlow,
            outgoingFlow = d.totalOutgoingFlow,
            inValue,
            outValue,
            inOutText,
            areaName;

        areaName = "<div class='tooltipAreaName'>" + nodeName + "</div>";
        inValue = "<span class='tooltipValue'>" + numberWithCommas(incomingFlow)
            + "</span> <div class='tooltipTypeHolder'>进<br/>来自</div> <div class='tooltipAreaName'>"
            + inState + "</div><br/>";
        outValue = "<span class='tooltipValue'>" + numberWithCommas(outgoingFlow)
            + "</span> <div class='tooltipTypeHolder'>出<br/>去往</div> <div class='tooltipAreaName'>" + inState + "</div><br/>";
        inOutText = "<div class='tooltipInOutText'>" + inValue + outValue + "</div>";

        return areaName + inOutText;
    }

    // Moves the selected svg element to lay on top of other SVG elements under the
    // same parent element. Needed for changing the color of polygon stroke
    // on hover to prevent stroke overlaps.
    d3.selection.prototype.moveToFront = function () {
        return this.each(function () {
            this.parentNode.appendChild(this);
        });
    };

    //创建一个地图，支持地图的平移和缩放 并添加初始化图层
    function initMap() {

        var background;

        // Create the svg element to hold all the map features.  阻止鼠标事件
        svg = d3.select("#map").append("svg")
            .attr("width", "100%")
            .attr("height", "100%")
            .on("click", stopped, true);

        // MAP LAYERS ------------------------------------
        // Add a background layer for detecting pointer events 添加鼠标的事件
        background = svg.append("rect").attr("class", "background")
            .attr("width", "100%")
            .attr("height", "100%")
            .attr("fill", rgbArrayToString(colors.backgroundFill))
            .on("click", zoomToFullExtent);

        var mapFeaturesLayer = svg.append("g").attr("id", "mapFeaturesLayer"),//用来添加专题图层
            statesLayer = mapFeaturesLayer.append("g").attr("id", "statesLayer"),//州图层，对应区图层
            countiesLayer = mapFeaturesLayer.append("g").attr("id", "countieslayer");//县图层，对应

        mapFeaturesLayer.append("g").attr("id", "flowsLayer");//流图
        mapFeaturesLayer.append("g").attr("id", "pointsLayer");//点图层


        //窗口调整
        $(window).resize(function () {
            windowWidth = this.innerWidth;
            windowHeight = this.innerHeight;
            svg.attr("width", "100%").attr("height", "100%");
            background.attr("width", "100%").attr("height", "100%");
        });

        // This line enables free zooming.
        // Also disables the double-click zooming default behavior.
        svg.call(zoom).on("dblclick.zoom", null).call(zoom.event);
        d3.json("xingzhenqu_polygon.geojson", function (error, us) {
            if (error) {
                throw error;
            }
            var model_master = Flox.getModel();

            //通过投影函数生成地理路径生成器

            statesLayer.selectAll("path").data(us.features)
                .enter().append("path")
                .attr("d", path)
                .attr("id", function (d) {
                    return "FIPS" + Number(d.properties.OBJECTID);
                })
                .attr("class", "feature state") // TODO neither feature or state classes really do anything
                .attr("stroke", function (d) {
                    return rgbArrayToString(colors.polygonStroke);
                })
                .style("stroke-linejoin", "bevel")
                .style("stroke-width", stateStrokeWidth)
                .attr("fill", "#ccc") // The initial fill before it's assigned.
                .on("click", stateClicked)
                .on("mouseover", function (d) {
                    showTooltip();
                    d3.select(this)
                        .style("stroke", function (d) {
                            // It is in county or state view mode?
                            if ((Flox.isCountyMode() && Flox.getSelectedState() !== false) ||
                                d3.select(this).style("fill") === rgbArrayToString(colors.unselectedStateFill)) {
                                // The states are that light gray color.
                                return rgbArrayToString(colors.unselectedStateHoverStroke);
                            }
                            return rgbArrayToString(colors.polygonHoverStroke);
                        })
                        .style("stroke-width", function (d) {
                            if (Flox.isCountyMode() && Flox.getSelectedState() === d.properties.STATEFP) {
                                return stateStrokeWidth;
                            } else {
                                return stateHoverStrokeWidth;
                            }

                        })
                        .moveToFront();
                })
                .on("mousemove", function (d) {
                    tooltip.html(function () {
                        return buildAreaTooltipText(d, model_master);
                    })
                        .style("left", (d3.event.pageX + tooltipOffset.x) + "px")
                        .style("top", function () {
                            var tooltipHeight = d3.select(this).node().getBoundingClientRect().height;
                            return (d3.event.pageY - tooltipHeight) + "px";
                        });
                })
                .on("mouseout", function (d) {
                    hideTooltip();
                    d3.select(this)
                        .style("stroke", function (d) {
                            //var settings = Flox.getFilterSettings();
                            if ((Flox.isCountyMode() && Flox.getSelectedState() !== false) ||
                                d3.select(this).style("fill") === rgbArrayToString(colors.unselectedStateFill)) {
                                return rgbArrayToString(colors.unselectedStateStroke);
                            }
                            return rgbArrayToString(colors.polygonStroke);
                        })
                        .style("stroke-width", stateStrokeWidth)
                        .classed("hovered", false);
                });
            // 加载县级别的图形
            d3.json("jiedao_polygon.geojson", function (error, us) {
                if (error) {
                    throw error;
                }

                var model_master = Flox.getModel();
                countiesLayer.selectAll("path")
                    .data(us.features)
                    .enter().append("path").attr("d", path)
                    .attr("class", function (d) {
                        return "feature county hidden FIPS" + Number(d.properties.OBJECTID_2) + " belongs";
                    })
                    .attr("fill", "#ccc")
                    .attr("stroke", function (d) {
                        return rgbArrayToString(colors.polygonStroke);
                    })
                    .style("stroke-width", 1) // TODO hardcoded value
                    .style("stroke-linejoin", "bevel")
                    .on("mouseover", function (d) {
                        showTooltip();
                        // highlightLegendRectangle(d);
                        d3.select(this)
                            .moveToFront()
                            .style("stroke", function (d) {
                                return rgbArrayToString(colors.polygonHoverStroke);
                            })
                            .style("stroke-width", 2) // TODO hardcoded value
                            .classed("hovered", true);
                    })
                    .on("mousemove", function (d) {
                        tooltip.html(function () {
                            return buildAreaTooltipText(d, model_master);
                        })
                            .style("left", (d3.event.pageX + tooltipOffset.x) + "px")
                            .style("top", function () {
                                var tooltipHeight = d3.select(this).node().getBoundingClientRect().height;
                                return (d3.event.pageY - tooltipHeight) + "px";
                            });
                    })
                    .on("mouseout", function () {
                        hideTooltip();
                        // unhighlightLegendRectangles();
                        d3.select(this)
                            .moveToFront()
                            .style("stroke", function (d) {
                                return rgbArrayToString(colors.polygonStroke);
                            })
                            .style("stroke-width", 1) // TODO hardcoded value
                            .classed("hovered", false);
                    })
                    .on("click", function (d) {
                        console.log("county clicked!");
                        countyClicked(d);
                    });
            });
        });
    }// End initpMap();

    function removeAllFlows() {
        d3.select("#flowsLayer").selectAll("g").remove();
        d3.select("#pointsLayer").selectAll("circle").remove();
        //d3.select("#necklaceMapLayer").remove();
    }

    function removeNecklaceMap() {
        d3.select("#necklaceMapLayer").remove();
    }


    function getNodeRadius(node) {
        return model_copy.getNodeRadius(node);
    }

    function endClipRadius(endNode) {
        // distance between end of flow and end point
        var gapDistanceToEndNode = model_copy.settings.flowDistanceFromEndPointPixel,
            endNodeRadius = model_copy.settings.NODE_STROKE_WIDTH / 2 + getNodeRadius(endNode);
        return gapDistanceToEndNode + endNodeRadius;
    }

    function startClipRadius(startNode) {
        // distance between end of flow and end point
        var gapDistanceToStartNode = model_copy.settings.flowDistanceFromStartPointPixel,
            startNodeRadius = model_copy.settings.NODE_STROKE_WIDTH / 2 + getNodeRadius(startNode);
        return gapDistanceToStartNode + startNodeRadius;
    }

    // takes a flow object, builts an SVG curve out of the 3 points, translating
    // the LatLng coordinates to screen coordinates. Also handles flow clipping.
    // Accounts for necklace map nodes.
    function buildSvgFlowPath(f, drawArrows) {

        var rs,
            re,
            flow,
            sPX,
            sPY,
            cPX,
            cPY,
            ePX,
            ePY;

        flow = f;
        sPX = flow.getStartPt().x;
        sPY = flow.getStartPt().y;

        cPX = flow.getCtrlPt().x;
        cPY = flow.getCtrlPt().y;

        ePX = flow.getEndPt().x;
        ePY = flow.getEndPt().y;

        return "M" + sPX + "," + sPY + " Q" + cPX + "," + cPY + " " + ePX + "," + ePY;
    }

    function buildSvgArrowPath(f) {
        var a = f.getArrow(),
            s;

        s = "M " + a.basePt.x + "," + a.basePt.y +
            " L" + a.corner1Pt.x + "," + a.corner1Pt.y +
            " Q" + a.corner1cPt.x + "," + a.corner1cPt.y +
            " " + a.tipPt.x + "," + a.tipPt.y +
            " Q" + a.corner2cPt.x + "," + a.corner2cPt.y +
            " " + a.corner2Pt.x + "," + a.corner2Pt.y +
            " L" + a.basePt.x + "," + a.basePt.y;

        return s;
    }

    function getFlowColor(flow) {
        var w, c;
        if (flow.isSelected()) {
            // return selectedColor;
        }
        if (flow.isLocked() && model_copy.settings.showLockedFlows) {
            // return lockedColor;
        }
        w = model_copy.getRelativeFlowValue(flow);

        c = Flox.ColorUtils.blend(colors.minFlowStroke, colors.maxFlowStroke, w);
        return rgbArrayToString(c);
        //return "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")";
    }

    /**
     * Configures the arrows so they will be scaled according to a model with
     * more flows in it than the maxFlows model passed into drawFeatures. This
     * allows arrows to be drawn the correct size when viewing individual
     * county flows.
     */
    function configureArrows(model) {
        var flows, flow, arrowSettings, i, j;
        // get the flows from the model_copy...
        flows = model_copy.getLargestFlows();
        for (i = 0, j = flows.length; i < j; i += 1) {
            flow = flows[i];
            // ...but get the settings from the activeModel.
            // This allows for proper sizing of arrows even though not all
            // flows are present in the model_copy
            arrowSettings = model.getArrowSettings(flow);
            flow.configureArrow(arrowSettings);
        }
    }


    function drawFlows(drawArrows) {

        var flows,
            i,
            j,
            f,
            rs,
            re,
            svgFlows;

        configureArrows(model_copy);

        // if (Flox.isStateMode() && Flox.getSelectedState() !== false) {
        // model_copy.settings.maxFlows = 60;
        // } else {
        // model_copy.settings.maxFlows = 50;
        // }

        flows = model_copy.getLargestFlows();

        // sort the flows in descending order so that big flows are drawn under
        // small flows (big flows will be drawn first)
        flows.sort(function (a, b) {
            return b.getValue() - a.getValue();
        });

        // called svgFlows because flows is already taken!
        svgFlows = d3.select("#flowsLayer").append("g").attr("id", "svgFlows")
            .selectAll("g")// a group for each flow
            .data(flows)// flow data added to GROUP
            .enter().append("g");


        // Draw an invisible version of each flow to handle hover events.
        // It's slightly wider in order to make hovering easier.
        svgFlows.append("path")
            .classed("curveOutline", true)//这个是背景色 是为了hover事件能清楚显示二维的
            .attr("stroke", "#ccc")
            .style("cursor", "default")
            .attr("fill", "none")
            .attr("stroke-width", function (d) {
                // The 4 in the code below is that makes the
                // invisible flow a bit wider for easier hovering.
                return (model_copy.getFlowStrokeWidth(d) + 4 * model_copy.settings.scaleMultiplier);
            })
            .attr("opacity", 0.0)
            .attr("d", function (d) {
                return buildSvgFlowPath(d, drawArrows);
            });

        // Draw arrowheads
        if (drawArrows) {
            svgFlows.append("path")// add a new path. This is the arrowhead!
                .classed("arrow", true)
                .style("cursor", "default")
                .attr("stroke", "none")
                .attr("fill", function (d) {
                    return getFlowColor(d);
                    //return "none";
                })
                .attr("stroke-width", 5)
                .attr("d", function (d) {
                    return buildSvgArrowPath(d);
                });
        }

        // Draw flow curves
        svgFlows.append("path")
            .classed("curve", true)
            .attr("stroke", function (d) {
                return getFlowColor(d);
            })
            .style("cursor", "default")
            .attr("fill", "none")
            .attr("stroke-width", function (d) {
                //对线的粗细进行分类
                var strokeWidth = model_copy.getFlowStrokeWidth(d);
                // If the stroke width is below the minimum, make it the
                // minimum and dashed 如果是最小的 或者是比做小的还小 那就虚线表示了
                if (strokeWidth <= model_copy.settings.minFlowWidth * model_copy.settings.scaleMultiplier) {
                    d3.select(this).attr("stroke-dasharray", strokeWidth * 6 + "," + strokeWidth * 3);
                }
                return strokeWidth;
            })
            .attr("d", function (d) {
                return buildSvgFlowPath(d, drawArrows);
            });


        svgFlows.on("mouseover", function (d) {
            showTooltip();
            var hoverColor = rgbArrayToString(colors.flowHoverStroke);
            d3.select(this).select(".curve").attr("stroke", hoverColor);
            d3.select(this).select(".arrow").attr("fill", hoverColor);
        })
            .on("mousemove", function (d) {
                tooltip.html(function () {
                    return buildFlowTooltipText(d);
                })
                    .style("left", (d3.event.pageX + tooltipOffset.x) + "px")
                    .style("top", function () {
                        var tooltipHeight = d3.select(this).node().getBoundingClientRect().height;
                        return (d3.event.pageY - tooltipHeight) + "px";
                    });
            })
            .on("mouseout", function () {
                hideTooltip();
                d3.select(this).select(".curve").attr("stroke", function (d) {
                    return getFlowColor(d);
                });
                d3.select(this).select(".arrow").attr("fill", function (d) {
                    return getFlowColor(d);
                });
            });
    }

    function drawPoints() {
        var points = model_copy.getPoints(),
            // points = model_copy.getNonNecklaceNodes(),
            circles = d3.select("#pointsLayer")
                .selectAll("circle")
                .data(points)
                .enter().append("circle");

        // Add some attributes to the points
        circles.style("stroke", "black").style("stroke-width", function (d) {
            return model_copy.settings.NODE_STROKE_WIDTH;
        })
            .attr("class", function (d) {
                return "node FIPS" + d.id; // FIXME id is really FIPS
            })
            .style("fill", "white")
            .style("stroke", function (d) {// adjust the color
                if (d.selected) {
                    return "#59A4FF";
                }
                return "black";
            })
            .style("cursor", "default")
            .attr("r", function (d) {
                return model_copy.getNodeRadius(d);
            })
            .attr("cx", function (d) {
                return d.x;
            })
            .attr("cy", function (d) {
                return d.y;
            });
    }

    /**
     * For debugging.
     * Draws obstacles, which includes nodes and circles around arrows. Good
     * for making sure flows don't overlap them.
     */
    function drawObstacles() {
        var obstacles = Flox.getObstacles(model_copy),
            circles;

        d3.select("#obstaclesLayer").remove();

        circles = d3.select("#mapFeaturesLayer").append("g").attr("id", "obstaclesLayer")
            .selectAll("circle")
            .data(obstacles)
            .enter().append("circle");

        circles.style("stroke", "black")
            .style("fill", "C80000")
            .style("opacity", 0.1)
            .attr("cx", function (d) {
                return d.x;
            })
            .attr("cy", function (d) {
                return d.y;
            })
            .attr("r", function (d) {
                return (d.r + model_copy.settings.minObstacleDistPx);
            })
            .attr("pointer-events", "none");
    }

    // TODO name better
    function getColorRampForD3(classes) {
        var colorRamp, d3ColorRamp = [], i;

        colorRamp = Flox.ColorUtils.getColorRamp(colors.minPopDensityFill,
            colors.maxPopDensityFill, classes);

        for (i = 0; i < colorRamp.length; i += 1) {
            d3ColorRamp.push(rgbArrayToString(colorRamp[i]));
        }
        return d3ColorRamp;
    }

    function colorCountiesByPopulationDensity() {
        var stateFIPS, counties, nodes, node, countyNodes = [], i,
            popDensities, model_master, popDensity, jenksBreaks,
            classes = defaultClasses;

        model_master = Flox.getModel();
        nodes = model_master.getPoints(); // this gets state and county nodes.
        stateFIPS = model_copy.settings.datasetName.slice(4);
        counties = d3.selectAll(".FIPS" + stateFIPS);

        for (i = 0; i < nodes.length; i += 1) {
            node = nodes[i];
            if (node.type === "county") {
                countyNodes.push(node);
            }
        }

        popDensities = countyNodes.map(function (d) {
            popDensity = Number(d.totalIncomingFlow);
            if (isNaN(popDensity)) {
                console.log("pop density is NaN!");
                return 0;
            }
            return popDensity;
        });

        if (popDensities.length <= defaultClasses) {
            // There are as many or more colors than there are counties
            // There will be only one county of each color.
            // Therefore, don't make a jenks scale. Just
            // use the sorted popDensities as breaks.
            popDensities.sort(function (a, b) {
                return a - b;
            });
            jenksBreaks = popDensities;
            classes = popDensities.length;
            populationDensityColor = d3.scale.threshold()
                .domain(jenksBreaks.slice(1))
                .range(getColorRampForD3(classes));
        } else {
            jenksBreaks = ss.jenks(popDensities, classes);
            populationDensityColor = d3.scale.threshold()
                .domain(jenksBreaks.slice(1, -1))
                .range(getColorRampForD3(classes));
        }


        configureLegend(jenksBreaks);

        counties.style("fill", function (d) {
            node = model_master.findNodeByID(Number(d.properties.OBJECTID));
            popDensity = Number(node.totalIncomingFlow);
            if (isNaN(popDensity) === false) {
                var color = populationDensityColor(popDensity);
                return color;
            }
            return "ccc";
        });
    }

    function colorStatesByPopulationDensity() {
        // Select the state polygons somehow.
        var statePolygons, model_master, nodes, node, i, popDensities, STATEFP,
            colorRamp, jenksBreaks;

        statePolygons = d3.selectAll(".feature.state");
        model_master = Flox.getModel();
        nodes = model_master.getPoints(); // This gets county AND state points.

        debugger
        // Get an array of all the population densities nodes.
        popDensities = nodes.map(function (d) {
            return Number(d.totalIncomingFlow);
        });

        // Create a jenks natural breaks scale with 7 classes.
        // Uses simple_statistics.js
        debugger
        jenksBreaks = ss.jenks(popDensities, defaultClasses)

        populationDensityColor = d3.scale.threshold()
            .range(getColorRampForD3(defaultClasses))
            .domain(jenksBreaks.slice(1, -1));

        configureLegend(jenksBreaks);

        statePolygons.style("fill", function (d) {
            STATEFP = d.properties.OBJECTID;
            node = model_master.findNodeByID((Number(STATEFP)));
            var color = populationDensityColor(Number(node.totalIncomingFlow));
            return color;
        })
            .style("stroke", rgbArrayToString(colors.polygonHoverStroke))
            .attr("opacity", 1);

    }

    function drawIntermediateFlowPoints() {
        // flow points should be stored inside flows, right`
        var flows,
            flowPoints,
            allFlowPoints = [],
            flow,
            circles,
            i, j;

        model_copy.cacheAllFlowLineSegments();
        flows = model_copy.getLargestFlows();

        for (i = 0; i < flows.length; i += 1) {
            flowPoints = flows[i].getCachedLineSegments();
            for (j = 0; j < flowPoints.length; j += 1) {
                allFlowPoints.push(flowPoints[j]);
            }
        }

        d3.select("#flowPointsLayer").remove();

        circles = d3.select("#mapFeaturesLayer").append("g").attr("id", "flowPointsLayer")
            .selectAll("circle")
            .data(allFlowPoints)
            .enter().append("circle");

        circles.style("stroke", "black")
            .style("fill", "white")
            .attr("cx", function (d) {
                return d.x;
            })
            .attr("cy", function (d) {
                return d.y;
            })
            .attr("r", function (d) {
                return 5 * model_copy.settings.scaleMultiplier;
            });
    }

    /**
     * @param m : A copy of the model.
     */
    function drawFeatures(m) {

        // Delete all the old flows before drawing new ones.
        removeAllFlows();

        if (!m) {
            throw new Error("drawFeatures needs to be passed a copy of the model");
        }

        // Store m in a place where other functions can get at it.
        // It's called model_copy as a reminder that it isn't the master model,
        // but a modified copy created by the filter.
        model_copy = m;

        if (model_copy.settings.drawFlows) {
            drawFlows(model_copy.settings.drawArrows);
        }
        model_copy.settings.drawNodes = true;
        if (model_copy.settings.drawNodes) {

            console.log("The model says to draw nodes, so I'm drawing nodes.");
            drawPoints();
        }
    }

    // TODO Are counties the only features that have a state fips as a class?
    // This seems like it will cause conflict later.
    function showCountyBordersWithinState(stateFIPS) {
        d3.selectAll(".FIPS" + Number(stateFIPS)).classed("hidden", false);
    }

    function hideAllCountyBorders() {
        d3.selectAll(".county").classed("hidden", true);
    }


    // Returns x, y, and radius of a circle containing a bounding box.
    function getCircleAroundBoundingBox(bb) {

        // Center point of bb
        var left = bb[0][0],
            right = bb[1][0],
            top = bb[0][1],
            bottom = bb[1][1],

            dx = right - left, // right - left
            dy = bottom - top, // bottom - top
            x = left + (dx / 2), // left + dx/2
            y = top + (dy / 2), // top + dy/2

            // Radius of the circle? This would be the distance from the center
            // point to any corner.
            dist = Math.sqrt((dx / 2) * (dx / 2) + (dy / 2) * (dy / 2)),
            centerXY;

        // TODO make the r slightly larger. Perhaps 10%

        return {
            cx: x,
            cy: y,
            r: dist
        };
    }

    function getCirclesAtBoundingBoxCorners(bb) {
        var leftTop,
            leftBottom,
            rightTop,
            rightBottom;

        leftTop = {
            cx: bb[0][0],
            cy: bb[0][1],
            r: 50
        };
        leftBottom = {
            cx: bb[0][0],
            cy: bb[1][1],
            r: 50
        };
        rightTop = {
            cx: bb[1][0],
            cy: bb[0][1],
            r: 50
        };
        rightBottom = {
            cx: bb[1][0],
            cy: bb[1][1],
            r: 50
        };
        return [leftTop, leftBottom, rightTop, rightBottom];
    }


    /**
     * Gets the center point of a d3-style bounding box, where:
     * [[left, top][right, bottom]]
     *
     * Returns: object {x,y}
     */
    function getCenterOfBoundingBox(bb) {
        // Center point of bb
        var left = bb[0][0],
            right = bb[1][0],
            top = bb[0][1],
            bottom = bb[1][1],

            dx = right - left, // right - left
            dy = bottom - top, // bottom - top
            x = left + (dx / 2), // left + dx/2
            y = top + (dy / 2);
        // top + dy/2

        return {
            x: x,
            y: y
        };
    }

    /**
     * Change the coordinates of pt so that it lies on
     * the intersection between the circle and a line drawn between pt and
     * the circle center.
     */
    function projectPointOnCircle(pt, circle) {
        var d,
            dx,
            dy,
            x,
            y;

        // Get the distance between the points
        dx = pt.x - circle.cx;
        dy = pt.y - circle.cy;
        d = Math.sqrt(dx * dx + dy * dy);

        // Pt is the center of circle. Return Pt.
        // TODO could return some point on the outside of circle
        if (d === 0) {
            return pt;
        }

        // Point is inside the circle. Return Pt.
        if (d <= circle.r) {
            //console.log("pt is inside the circle");
            return pt;
        }

        pt.x = circle.cx + (dx * circle.r / d);
        pt.y = circle.cy + (dy * circle.r / d);

        return pt;
    }

    function getMaxOutOfStateNetFlow(model) {
        // Get the nodes
        var nodes = model.getPoints(),
            maxNetFlow = 0, i, netFlow;
        // looooopy loop-de-looper loop loop
        for (i = 0; i < nodes.length; i += 1) {
            netFlow = Math.abs(nodes[i].netFlow);
            maxNetFlow = netFlow > maxNetFlow ? netFlow : maxNetFlow;
        }
        return maxNetFlow;
    }

    // TODO could I make one function that does both this and the above one?
    function getMaxOutOfStateTotalFlow() {
        // Get the nodes
        var nodes = Flox.getModel().getPoints(),
            maxTotalFlow = 0, i, totalFlow;
        // looooopy loop-de-looper loop loop
        for (i = 0; i < nodes.length; i += 1) {
            totalFlow = nodes[i].totalIncomingFlow + nodes[i].totalOutgoingFlow;
            maxTotalFlow = totalFlow > maxTotalFlow ? totalFlow : maxTotalFlow;
        }
        return maxTotalFlow;
    }

    function sortCirclesByRadius(circles) {
        circles.sort(function (a, b) {
            return b.r - a.r;
        });
    }

    /**
     * Places circles on outerCircle that correspond to the states array.
     *
     * @param {Object} outerCircle - Circle around central state polygon
     * @param {Array} states - Array of state abbreviations to make circles for.
     */
    function getStateCircles(model, outerCircle, states, outerStatesNodes) {

        var stateCircles = [],
            nodeValue,
            maxNodeValue = getMaxOutOfStateTotalFlow(),
            maxR = outerCircle.r * 0.2,
            maxArea = Math.PI * maxR * maxR,
            ptArea,
            i,
            j,
            pt;

        for (i = 0, j = outerStatesNodes.length; i < j; i += 1) {
            pt = outerStatesNodes[i];
            projectPointOnCircle(pt, outerCircle);
            pt.necklaceMapNode = true;

            // Make node radius based on total flow for that node.
            ptArea = (maxArea * (pt.totalIncomingFlow + pt.totalOutgoingFlow)) / maxNodeValue;
            pt.r = Math.sqrt(ptArea / Math.PI);

            pt.strokeWidth = pt.r * 0.10;
            stateCircles.push(pt);
        }
        // Sort by radius so they can be drawn in order by size
        sortCirclesByRadius(stateCircles);
        return stateCircles;
    }

    /**
     * Zooms in to the provided json-based d3-style feature object. Needs
     * to be compatible with the d3.geo.path().bounds(d) function.
     * d is the polygon to zoom to.
     */
    function zoomToPolygon(d) {

        // Get a bb (bounding box) around the polygon.
        // TODO path is global to the mapComponent namespace, and this is the only
        // function that uses it.
        var bounds = path.bounds(d),
            dx = bounds[1][0] - bounds[0][0],
            dy = bounds[1][1] - bounds[0][1],
            x = (bounds[0][0] + bounds[1][0]) / 2,
            y = (bounds[0][1] + bounds[1][1]) / 2,

            // Set the map scale to fit the larger side of the bb.
            // Adds a little padding to the outside.
            scale = 0.6 / Math.max(dx / windowWidth, dy / windowHeight),

            // translate is an [x,y] representing the coordinates at the
            // center of the bb.
            translate = [windowWidth / 2 - scale * x, windowHeight / 2 - scale * y];

        // Zoom to the new scale and bb center.
        svg.transition()
            .duration(750)
            .call(zoom.translate(translate).scale(scale).event);
    }

    // rect is {x, y, height, width}
    function zoomToRectangle(rect) {
        var dx = rect.w,
            dy = rect.h,
            x = (rect.x + dx) / 2,
            y = (rect.y + dy) / 2,
            scale = 0.6 / Math.max(dx / windowWidth, dy / windowHeight),
            translate = [windowWidth / 2 - scale * x, windowHeight / 2 - scale * y];
        svg.transition()
            .duration(750) // TODO long zoom for testing asynchronous stuff.
            .call(zoom.translate(translate).scale(scale).event);
    }

    // Zooms to a circle object {cx: center x, cy: center y, r: radius}
    // Adds a litle padding to the circle and the edge of the window
    function zoomToCircle(c) {
        var dx = c.r * 2,
            dy = c.r * 2,
            padding = 0.65, // The further below 1, the more padding it gets.
            scale = padding / Math.max(dx / windowWidth, dy / windowHeight),
            translate = [windowWidth / 2 - scale * c.cx, windowHeight / 2 - scale * (c.cy) - windowHeight * 0.05];
        svg.transition()
            .duration(750) // TODO long zoom for testing asynchronous stuff.
            .call(zoom.translate(translate).scale(scale).event);
    }


    // FIXME Hardcoded extent. Calculate scale from features instead.
    // ...Except this is optimized to make room for the UI at the bottom of
    // the window. So keep it for now.
    function zoomToFullExtent() {
        zoomToRectangle({x: -8000, y: -5000, w: 9000, h: 7000})
    }

    /**
     * Does all the things needed when a state is selected. Makes a necklace
     * map, zooms in, lays out the flows, displays the flows, everything.
     * @param {object} Object containing the geometry and properties of the
     * selected state.
     */
    function selectState(stateFIPS) {

        var statePolygon, stateBoundingBox, outerCircle, testStates,
            stateCircles;

        // Clear out all flows and necklace maps.
        removeAllFlows();
        d3.select("#necklaceMapLayer").remove();

        // get the statePolygon, yes? This is goofy here. You want the data though.
        // How else would you get it?
        d3.select("#" + "FIPS" + Number(stateFIPS)).each(function (d) {
            statePolygon = d;
        });

        // Behavior should be different if it's in stateMode vs countyMode. 为了画出neckLace
        //var filterSettings = Flox.getFilterSettings();

        // Deselect the selected county if there is one
        // Flox.filterSettings.selectedCounty = true;
        // Flox.setSelectedCounty(true);
        // Flox.filterSettings.selectedState = stateFIPS;
        // Flox.filterSettings.selectedFeatureName = statePolygon.properties.XZQ_NAME;


        //Flox.setSelectedCounty(false);
        Flox.setSelectedState(stateFIPS);
        Flox.setSelectedFeatureName(statePolygon.properties.XZQ_NAME);

        // County mode?
        if (Flox.isCountyMode()) {

            // Hide the tooltip
            my.disableTooltip();
            hideTooltip();

            Flox.setSelectedCounty(false);

            // Get the smallest circle around the polygon, save it for later.
            console.log(statePolygon);
            outerCircle = getSmallestCircleAroundPolygon(statePolygon);

            // Load the county flow data for that state
            //zoomToPolygon(statePolygon);

            //计算圆只是为了缩放？
            zoomToCircle(outerCircle);

            Flox.importTotalCountyFlowData(statePolygon.properties.OBJECTID);
            // Hide county boundaries
            hideAllCountyBorders();
            // Show just the county boundaries for the selected state
            showCountyBordersWithinState(statePolygon.properties.OBJECTID);

            d3.selectAll(".feature.state")
                .transition()
                .style("fill", rgbArrayToString(colors.unselectedStateFill))
                .style("stroke", rgbArrayToString(colors.unselectedStateStroke));

        } else if (Flox.isStateMode()) {
            // Load the state flow data for that state.
            // Because it's in state mode, the state flows should already be
            // loaded. So all it has to do is show the flows.
            Flox.updateMap();
        }


    }

    function selectCounty(countyFIPS, feature) {
        removeAllFlows();
        d3.select("#necklaceMapLayer").remove();
        var lsad = feature.properties.LSAD;
            // featureType = lookupLSAD[lsad];

        // Flox.setFilterSettings({
        // selectedCounty: countyFIPS,
        // selectedFeatureName: feature.properties.NAME + " " + featureType});

        Flox.setSelectedCounty(countyFIPS);
        Flox.setSelectedFeatureName(feature.properties.XZQ_NAME );

        Flox.updateMap();
    }

    function stateClicked(d) {

        // If it's in county mode and this is the selected state, do nothing.
        if (Flox.isCountyMode() && Flox.getSelectedState() === d.properties.OBJECTID) {
            ``
            return;
        }

        // If it's in state mode, and this state is already selected...reset?
        if (Number(Flox.getSelectedState()) === Number(d.properties.OBJECTID)) {//如果还是选择的那个州的话
            hideTooltip();
            my.disableTooltip();
            zoomToFullExtent();
            // load the state to state flows?
            Flox.setSelectedState(false);
            Flox.importStateToStateMigrationFlows();
        } else {//如果不是 那就换一个
            //分级别
            selectState(d.properties.OBJECTID);
        }
    }

    function countyClicked(d) {
        var FIPS = d.properties.OBJECTID;
        selectCounty(FIPS, d);
    }

    function necklaceNodeClicked(d) {
        selectState(d.STATEFP);
    }


    function zoomed() {
        var g = svg.select("#mapFeaturesLayer");

        mapScale = d3.event.scale;

        g.attr("transform", "translate(" + d3.event.translate + ")scale(" + mapScale + ")");
    }

    // If the drag behavior prevents the default click,
    // also stop propagation so we don’t click-to-zoom.
    function stopped() {
        if (d3.event.defaultPrevented)
            d3.event.stopPropagation();
    }

    /**
     * Places nodes on the outer rim of circle, using d3 force directed graph
     * to lay out the nodes.
     * @param {Object} nodes
     * @param {Object} circle
     */
    function addNecklaceMap(outerCircle, stateNodes, callback) {
        var w = 0, // width of force graph.
            h = 0, // height of force graph.
            // get the radius of the largest node, which is the first on in the
            // array of stateNodes (it gets sorted in getStateCircles)
            nodeRadius = stateNodes[0].r,
            // center of circle the points must stay outside of.
            cx = outerCircle.cx, // center x of circle nodes stay out of
            cy = outerCircle.cy, // center y of circle nodes stay out of
            // radius of the circle the necklace
            // nodes are arranged around. Radius of the largest node is added
            // to keep necklace nodes from overlapping the state.
            ringR = outerCircle.r + nodeRadius,
            force, necklaceMap, nodes, i,
            labelSize,	// in pixels
            labelSize_max = (outerCircle.r * 0.07),
            labelSize_min = (outerCircle.r * 0.03),
            pt,
            labelOffset = 0;

        // delete the previous necklace map
        // TODO this should happen sooner, like when a state is clicked.
        d3.select("#necklaceMapLayer").remove();

        // Initialize the force layout settings
        // 0 gravity has great results! Otherwize nodes arrange themselves lopsided.
        force = d3.layout.force()
            .gravity(0)
            .friction(0.6)
            .charge(function (node) {
                // TODO Sometimes charge is too big, sometimes too small.
                var charge = -node.r * node.r / 6,
                    minCharge = -ringR * 0.1;

                if (charge > minCharge) {
                    return minCharge;
                }
                return charge;
                //return r;

            }).size([w, h])
            .nodes(stateNodes);

        // Add an SVG group to hold the necklace map.
        necklaceMap = d3.select("#mapFeaturesLayer").append("g").attr("id", "necklaceMapLayer");

        nodes = necklaceMap.selectAll(".necklaceNode")
            .data(stateNodes)
            .enter().append("g")
            .attr("class", "necklaceNode")
            .style("cursor", "default");

        // Load the data.
        nodes.append("circle")
            .attr("r", function (d) {
                return d.r;
            })
            .style("fill", function (d) {
                return rgbArrayToString(colors.necklaceNodeFill);
            })
            .style("stroke", function (d) {
                return rgbArrayToString(colors.necklaceNodeStroke);
            })
            .style("stroke-width", function (d) {
                return (d.strokeWidth);
            });
        // This is where dragable stuff happens
        // Enabled for development purposes.
        // TODO be sure to turn this off later.
        // .call(force.drag)
        // .on("mousedown", function() {
        // d3.event.stopPropagation();
        // });

        // Add some mouse interactions to the nodes, like tooltips.
        nodes.on("mouseover", function (d) {
            showTooltip();
            d3.select(this).selectAll("circle")
                .style("fill", function (d) {
                    return rgbArrayToString(colors.necklaceNodeHoverFill);
                });
        })
            .on("mousemove", function (d) {
                tooltip.html(buildNecklaceNodeTooltipText(d, outerCircle.name))
                    .style("left", (d3.event.pageX + tooltipOffset.x) + "px")
                    .style("top", function () {
                        var tooltipHeight = d3.select(this).node().getBoundingClientRect().height;
                        return (d3.event.pageY - tooltipHeight) + "px";
                    });
            })
            .on("mouseout", function () {
                hideTooltip();
                d3.select(this).selectAll("circle")
                    .style("fill", function (d) {
                        return rgbArrayToString(colors.necklaceNodeFill);
                    });
            })
            .on("click", function (d) {
                hideTooltip();
                necklaceNodeClicked(d);
            });

        // Detects collision between points I guess?
        // Copied from https://bl.ocks.org/mbostock/3231298
        function collide(node, minDistance) {
            var r = node.r,
                nx1 = node.x - r,
                nx2 = node.x + r,
                ny1 = node.y - r,
                ny2 = node.y + r;
            return function (quad, x1, y1, x2, y2) {
                if (quad.point && (quad.point !== node)) {
                    var x = node.x - quad.point.x,
                        y = node.y - quad.point.y,
                        l = Math.sqrt(x * x + y * y),
                        r = node.r + quad.point.r;
                    if (l < r) {
                        l = (l - r) / l * 0.5;
                        node.x -= x *= l;
                        node.y -= y *= l;
                        quad.point.x += x;
                        quad.point.y += y;
                    }
                }
                return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
            };
        }

        function tick(alpha) {
            var dx, dy, dist;

            // Try out the collision algorithm!
            if (alpha.alpha < 0.085) {
                var q = d3.geom.quadtree(stateNodes),
                    i = 0,
                    n = stateNodes.length;

                while (++i < n) q.visit(collide(stateNodes[i], ringR * 0.05));
            }

            // Changing the transform moves everything in the group.
            nodes.attr("transform", function (d) {
                dx = d.x - cx;
                dy = d.y - cy;
                dist = Math.sqrt(dx * dx + dy * dy);
                d.x = dx * ringR / dist + cx;

                dx = d.x - cx;
                dy = d.y - cy;
                dist = Math.sqrt(dx * dx + dy * dy);
                d.y = dy * ringR / dist + cy;
                return "translate(" + d.x + "," + d.y + ")";
            });
        }

        // On each tick of the force layout, run tick()
        force.on("tick", tick);

        // Start the force layout.
        // Number of ticks increases with number of nodes.
        // FIXME this is not optimal. Sometimes it's not enough ticks,
        // sometimes too many.
        force.start();
        for (i = stateNodes.length * 10; i > 0; i -= 1) {
            force.tick();
        }
        force.stop();

        //添加节点显示的文字 属性为Point的属性
        nodes.append("text")

            .text(function (d) {
                // console.log(d);
                return d.name;
            })
            .each(function (d) {
                pt = d3.select(this);

                // calculate the label size.
                labelSize = (d.r * 0.5);

                // if it's smaller than the min, make it the min and place
                // it outside the circle
                if (labelSize < labelSize_min) {
                    labelSize = labelSize_min;
                    pt.style("font-size", function (d) {
                        return labelSize + "px";
                    });
                    labelOffset = (d.r * -0.1);
                    if (d.x >= cx && d.y <= cy) { // top right
                        pt.attr("text-anchor", "start")
                            .attr("x", d.r + labelOffset)
                            .attr("y", -d.r - labelOffset);
                    }
                    if (d.x >= cx && d.y > cy) { // bottom right
                        pt.attr("text-anchor", "start")
                            .attr("dominant-baseline", "hanging")
                            .attr("x", d.r + labelOffset)
                            .attr("y", d.r + labelOffset);
                    }
                    if (d.x < cx && d.y <= cy) { // top left
                        pt.attr("text-anchor", "end")
                            .attr("x", -d.r - labelOffset)
                            .attr("y", -d.r - labelOffset);
                    }
                    if (d.x < cx && d.y > cy) { // bottom left
                        pt.attr("text-anchor", "end")
                            .attr("dominant-baseline", "hanging")
                            .attr("x", -d.r - labelOffset)
                            .attr("y", d.r + labelOffset);
                    }
                } else {
                    // It's bigger than the min because the circle is large
                    // enough to hold a label of the min size. Put the label
                    // inside the circle.
                    // If the label is bigger than the max, set it to the max,
                    if (labelSize > labelSize_max) {
                        labelSize = labelSize_max;
                    }
                    pt.style("font-size", function (d) {
                        return labelSize + "px";
                    })
                        .attr("text-anchor", "middle")
                        .attr("dominant-baseline", "central");
                }
            });
    }

    /**
     * Gets the smallest circle that encloses targetStatepolygon.
     *
     * 抄写的人家的源码计算的
     * Uses code copied from https://www.nayuki.io/page/smallest-enclosing-circle
     * targetStatePolygon is a d3.feature. Points from that feature are
     * converted from [x,y] tp {x: value, y: value} to be compatible
     * with the copied code, which kinda sucks.
     */
    function getSmallestCircleAroundPolygon(targetStatePolygon) {
        // Get the points from the polygon somehow?

        var points, polygons,
            formattedPoints = [],
            i, j, pt, xy, circle, formattedCircle;


        if (targetStatePolygon.geometry.type === "MultiPolygon") {//有两个面 如南山区

            polygons = targetStatePolygon.geometry.coordinates;

            for (i = 0; i < polygons.length; i += 1) {
                // polygons[0] is an array of one thing.
                // polygons[1] is also an array of one thing.
                // those one things are arrays of point arrays.
                points = polygons[i][0];
                for (j = 0; j < points.length; j += 1) {
                    xy = projection(points[j]);
                    if (xy !== null) {
                        formattedPoints.push({x: xy[0], y: xy[1]});
                    }
                }
            }


        } else {//一个面
            // do this other thing.
            points = (targetStatePolygon.geometry.coordinates[0]);
            for (i = 0, j = points.length; i < j; i += 1) {
                // convert latLng to pixel coords.
                xy = projection(points[i]);
                // turn the array of xy into an object of xy.
                // Push the new object into formattedPoints
                // convert the points to be compatible with smallest circle algorithm
                // FIXME this is dumb
                formattedPoints.push({x: xy[0], y: xy[1]});
            }
        }

        console.log(formattedPoints)
        circle = Flox.GeomUtils.makeCircle(formattedPoints);

        formattedCircle = {
            cx: circle.x,
            cy: circle.y,
            r: circle.r,
            name: targetStatePolygon.properties.XZQ_NAME
        };
        console.log(formattedCircle)
        return formattedCircle;
    }

    /**
     * Create a necklace map for the selected state. Flows entering or leaving
     * the selected state will have outer-state nodes replaced with necklace
     * map nodes. Only makes necklace map nodes for states that have displayed
     * flows (maxFlows only).
     * 添加环状显示
     * Uses d3 force layout to arrange nodes.
     * @param {Number} stateFIPS - US state FIPS code
     * @param {Object} model - FloxModel containing flows
     * @param {Function} callback - Called when finished.
     */
    function configureNecklaceMap(model) {

        var flows = model.getFlows(),
            outerStates = [],
            outerStatesNodes = [],
            flow,
            targetStatePolygon,
            stateBoundingBox,
            outerCircle,
            stateCircles,
            i, j, sPt, ePt,
            necklaceMapNodes,
            smallerOuterCircle,
            datasetName = model.settings.datasetName; // FIPS code of selected state

        // in the format "FIPS00"

        // Remove the existing necklace map.
        d3.select("#necklaceMapLayer").remove();

        // Get the polygon of the currently selected state, which contains
        // various needed parameters.
        d3.select("#" + datasetName).each(function (d) {
            targetStatePolygon = d;
        });

        // Loop through the flows in the model
        for (i = 0, j = flows.length; i < j; i += 1) {
            flow = flows[i];

            // Start point and end point
            sPt = flow.getStartPt();
            ePt = flow.getEndPt();

            // If the state FIPS codes of the start or end point don't match
            // the FIPS code of the selected state, add that FIPS code to an
            // array that tracks which states have out of state flows.
            if ("FIPS" + sPt.STATEFP !== datasetName
                && (outerStates.indexOf(sPt.STATEFP) < 0)) {
                outerStates.push(sPt.STATEFP);
                outerStatesNodes.push(sPt);
            }
            if ("FIPS" + ePt.STATEFP !== datasetName
                && (outerStates.indexOf(ePt.STATEFP) < 0)) {
                outerStates.push(ePt.STATEFP);
                outerStatesNodes.push(ePt);
            }
        }

        // Get the smallest circle that encloses the targetStatePolygon
        smallerOuterCircle = getSmallestCircleAroundPolygon(targetStatePolygon);

        // Get a circle for each outerState.
        // Actually moves the offending flow nodes and adds attributes r,
        // necklaceMapNode = true, and strokeWidth. I think that's it.
        stateCircles = getStateCircles(model, smallerOuterCircle, outerStates, outerStatesNodes);

        // If there are any stateCircles...
        if (stateCircles.length > 0) {
            // Create and add the necklace map to the map.
            addNecklaceMap(smallerOuterCircle, stateCircles, function (necklaceMapNodes) {
                //callback();
            });
        } else {
            console.log("No out of state nodes?");
        }
    }


    //刷信图例
    function configureLegend(legendData) {

        var rectWidth = 25,
            rectHeight = 15,
            rectSpacer = 5,
            labelSizePx = 13,
            flowLength = 62,
            flowSpacer = 18,
            s = model_copy.settings,
            flowLeft = 0 - s.flowDistanceFromStartPointPixel,
            flowLegendItemContainer,
            legendItemContainer,
            rectangle,
            legendItem,
            flowSVG,
            legendFlows = [],
            svgLegendFlows,
            maxFlow, maxFlowStartPt, maxFlowEndPt,
            midFlow, midFlowStartPt, midFlowEndPt,
            minFlow, minFlowStartPt, minFlowEndPt,
            legendColors = populationDensityColor.range(),
            i,
            largestFlows = model_copy.getLargestFlows(),
            arrowSettings,
            maxLegendFlowWidth = 10,
            minLegendFlowWidth = maxLegendFlowWidth / (s.maxFlowWidth / s.minFlowWidth),
            midLegendFlowWidth = minLegendFlowWidth + ((maxLegendFlowWidth - minLegendFlowWidth) * 0.5),
            maxFlowValue = s.useGlobalFlowWidth ? s.maxFlowValue : largestFlows[0].getValue(),
            dashedFlowValue = maxFlowValue / (s.maxFlowWidth / s.minFlowWidth),
            notShownFlowValue = largestFlows[largestFlows.length - 1].getValue(),
            midFlowValue = dashedFlowValue + ((maxFlowValue - dashedFlowValue) * 0.5),
            legendFlowLabels,
            flowGap,
            notShownText = "",
            percentShown,
            percentShownText = "";

        if (largestFlows.length >= s.maxFlows &&
            largestFlows.length < Flox.getCurrentFilteredModel().getFlows().length) {
            notShownText = "OD值小于 " + numberWithCommas(largestFlows[largestFlows.length - 1].getValue()) + "未标识";
            percentShown = Flox.getCurrentFilteredModel().getPercentageOfTotalFlowShown();
            percentShown = (Math.floor(percentShown * 10)) / 10;
            percentShownText = "" + (percentShown) + "% OD数据";
        }

        if (maxLegendFlowWidth / 2 > maxLegendFlowWidth * model_copy.settings.arrowWidthScaleFactor) {
            flowGap = (maxLegendFlowWidth / 2);
        } else {
            flowGap = maxLegendFlowWidth * model_copy.settings.arrowWidthScaleFactor;
        }

        flowGap += 3;

        // Remove the old legend svg. Title can stay.
        d3.select("#mapLegend").selectAll("svg").remove();
        d3.select("#mapLegend").selectAll("p").remove();
        // d3.select(".legendSectionSpacer").remove();

        // Make flows to show in the legend.
        maxFlowStartPt = {x: flowLeft, y: flowGap, value: 0, id: "legendMaxStart"};
        maxFlowEndPt = {x: flowLeft + flowLength, y: flowGap, value: 0, id: "legendMaxEnd"};
        maxFlow = new Flox.Flow(maxFlowStartPt, maxFlowEndPt, maxLegendFlowWidth, "maxLegendFlow");

        midFlowStartPt = {x: flowLeft, y: flowGap + flowSpacer, value: 0, id: "legendMidStart"};
        midFlowEndPt = {x: flowLeft + flowLength, y: flowGap + flowSpacer, value: 0, id: "legendMidEnd"};
        midFlow = new Flox.Flow(midFlowStartPt, midFlowEndPt, midLegendFlowWidth, "midLegendFlow");

        minFlowStartPt = {x: flowLeft, y: flowGap + flowSpacer * 2, value: 0, id: "legendMaxStart"};
        minFlowEndPt = {x: flowLeft + flowLength, y: flowGap + flowSpacer * 2, value: 0, id: "legendMaxEnd"};
        minFlow = new Flox.Flow(minFlowStartPt, minFlowEndPt, minLegendFlowWidth, "minLegendFlow");

        legendFlows = [maxFlow, midFlow, minFlow];

        d3.select("#mapLegend")
            .append("p")
            .classed("legendTitle", true)
            .html("OD值");

        // Append an svg to hold the flows
        flowLegendItemContainer = d3.select("#mapLegend")
            .append("svg")
            .attr("id", "flowsLegend")
            .attr("width", "100%")
            .attr("height", function () {
                var legendHeight = flowGap + ((legendFlows.length - 1) * flowSpacer) + (labelSizePx / 2) + 5;
                if (notShownText !== "") {
                    legendHeight += flowSpacer * 2;
                }
                return legendHeight;
            })
            .style("margin-top", -8)
            .style("background-color", "none");

        // Draw the flows in the legend!
        svgLegendFlows = flowLegendItemContainer.append("g").attr("id", "svgLegendFlows")
            .selectAll("g")
            .data(legendFlows)
            .enter().append("g");

        // Arrows first
        if (model_copy.settings.drawArrows) {
            // Configure the arrow for each flow
            for (i = 0; i < legendFlows.length; i += 1) {
                arrowSettings = model_copy.getArrowSettings(legendFlows[i]);
                arrowSettings.maxFlowWidth = maxLegendFlowWidth;
                arrowSettings.minFlowWidth = minLegendFlowWidth;
                arrowSettings.maxFlowValue = maxFlow.getValue();
                arrowSettings.scaleMultiplier = 1;
                legendFlows[i].configureArrow(arrowSettings);
            }
            svgLegendFlows.append("path")
                .classed("arrow", true)
                .style("cursor", "default")
                .attr("stroke", "none")
                .attr("fill", function (d) {
                    if (d.getId() === "minLegendFlow") {
                        return rgbArrayToString(colors.minFlowStroke);
                    }
                    if (d.getId() === "midLegendFlow") {
                        return rgbArrayToString(Flox.ColorUtils.blend(colors.maxFlowStroke, colors.minFlowStroke, 0.5));
                    }
                    return rgbArrayToString(colors.maxFlowStroke);
                })
                .attr("stroke-width", 5)
                .attr("d", function (d) {
                    return buildSvgArrowPath(d);
                });
        }

        // Now curves
        svgLegendFlows.append("path")
            .classed("curve", true)
            .attr("stroke", function (d) {
                if (d.getId() === "minLegendFlow") {
                    return rgbArrayToString(colors.minFlowStroke);
                }
                if (d.getId() === "midLegendFlow") {
                    return rgbArrayToString(Flox.ColorUtils.blend(colors.maxFlowStroke, colors.minFlowStroke, 0.5));
                }
                return rgbArrayToString(colors.maxFlowStroke);
            })
            .style("cursor", "default")
            .attr("fill", "none")
            .attr("stroke-width", function (d) {
                var strokeWidth = d.getValue();
                // If the stroke width is below the minimum, make it the
                // minimum and dashed
                if (d.getId() === "minLegendFlow") {
                    d3.select(this).attr("stroke-dasharray", strokeWidth * 6 + "," + strokeWidth * 3);
                }
                return strokeWidth;
            })
            .attr("d", function (d) {
                return buildSvgFlowPath(d, model_copy.settings.drawArrows);
            });

        // Flow labels!
        legendFlowLabels = [
            // max flow label
            numberWithCommas(Math.round(maxFlowValue)),
            numberWithCommas(Math.round(midFlowValue)),
            "<" + numberWithCommas(Math.round(dashedFlowValue))
        ];

        svgLegendFlows.append("text")
            .style("font-size", labelSizePx + "px")
            .style("font-family", "Tahoma, Geneva, sans-serif")
            .attr("fill", "black")
            .text(function (d, i) {
                return legendFlowLabels[i];
            })
            .attr("x", flowLeft + flowLength)
            .attr("y", function (d, i) {
                return flowGap + (flowSpacer * i) + labelSizePx / 2 - 2;
            })

        // If some flows aren't shown, add notShownText
        flowLegendItemContainer.append("text")
            .style("font-size", labelSizePx + "px")
            .style("font-family", "Tahoma, Geneva, sans-serif")
            .attr("fill", "black")
            .text(notShownText)
            .attr("x", flowLeft + s.flowDistanceFromStartPointPixel)
            .attr("y", function (d, i) {
                return flowGap + (flowSpacer * legendFlows.length) + labelSizePx / 2 + 2;
            })

        flowLegendItemContainer.append("text")
            .style("font-size", labelSizePx + "px")
            .style("font-family", "Tahoma, Geneva, sans-serif")
            .attr("fill", "black")
            .text(percentShownText)
            .attr("x", flowLeft + s.flowDistanceFromStartPointPixel)
            .attr("y", function (d, i) {
                return flowGap + (flowSpacer * (legendFlows.length + 1)) + labelSizePx / 2 + 2;
            })


        d3.select("#mapLegend")
            .append("p")
            .classed("legendTitle", true)
            .html("行政单元全天OD流入总数");

        // Append svg to hold the color swatches.
        legendItemContainer = d3.select("#mapLegend")
            .append("svg")
            .attr("id", "areasLegend")
            .attr("width", "100%")
            .attr("height", function () {
                var l = legendColors.length;
                return (l * rectHeight + (l - 1) * rectSpacer) + 3;
            })
            .style("margin-top", -5);

        legendItem = legendItemContainer.selectAll("g")
            .data(populationDensityColor.range())
            .enter().append("g")
            .classed("legendItem", true);

        legendItem.append("rect")
            .attr("width", rectWidth)
            .attr("height", rectHeight)
            .attr("fill", "black")
            .attr("y", function (d, i) {
                return 2 + (rectHeight + rectSpacer) * i;
            })
            .attr("x", 2)
            .attr("fill", function (d, i) {
                //return populationDensityColor.range()[i];
                return d;
            });

        legendItem.append("text")
            .style("font-size", labelSizePx + "px")
            .style("font-family", "Tahoma, Geneva, sans-serif")
            .attr("fill", "black")
            .text(function (d, i) {
                if (legendData.length <= defaultClasses) {
                    //One color for each value
                    return (Math.round(legendData[i] * 100) / 100);
                }
                // There are more values than colors.
                // legendData includes 8 values, where the first
                // and last are the min and max. Ranges!
                if (i < defaultClasses - 1) {
                    return (Math.round(legendData[i] * 100) / 100) + "——" + (Math.round(legendData[i + 1] * 100) / 100)
                }
                return (Math.round(legendData[i] * 100) / 100) + " —— " +
                    (Math.round(legendData[i + 1] * 100) / 100);
            })
            .attr("x", rectWidth + 10)
            .attr("y", function (d, i) {
                return 2 + (rectHeight / 2 + (labelSizePx / 2 - 1)) + ((rectHeight + rectSpacer) * i);
            });
    }


    //点亮分级图例
    function highlightLegendRectangle(d) {
        var legendRect,
            legendValues = populationDensityColor.domain(),
            node,
            featureValue;

        // select the legend rectangles.
        legendRect = d3.selectAll(".legendItem rect");


        // get the node value
        if (d.properties.COUNTYFP) {
            node = model_copy.findNodeByID(Number(d.properties.STATEFP) + d.properties.COUNTYFP);
        } else {
            // it's a state

            // If we're in county mode, do nothing! Abort!
            if (Flox.isCountyMode()) {
                return;
            }

            node = model_copy.findNodeByID(String(Number(d.properties.STATEFP)));
        }

        featureValue = Number(node.populationDensity);

        legendRect.each(function (d, i) {
            if (populationDensityColor(featureValue) === d) {
                d3.select(this)
                    .style("stroke-width", 1.5)
                    .style("stroke", "black");
            } else {
                d3.select(this)
                    .style("stroke-width", 0)
                    .style("stroke", "none");
            }
        });
    }


    //取消分级图例点亮
    function unhighlightLegendRectangles() {
        d3.selectAll(".legendItem rect")
            .style("stroke", "none");
    }


    //对外开放的方法
    // PUBLIC ---------------------------------------------------------------------
    my.configureNecklaceMap = function (m) {
        configureNecklaceMap(m);
    };


    my.drawFeatures = function (m) {
        drawFeatures(m);
    };

    my.initMap = function () {
        initMap();
    };

    my.removeAllFlows = function () {
        removeAllFlows();
    };

    my.removeNecklaceMap = function () {
        removeNecklaceMap();
    };

    my.clearAllMapFeatures = function () {
        removeAllFlows();
        d3.select("#necklaceMapLayer").remove();
    };

    my.latLngToLayerPt = function (latLng) {
        // Return the pixel coordinates of a latLng object.
        var xy = projection([latLng.lng, latLng.lat]);

        if (xy === null) {
            //console.log("latLngToLayerPt resulted in null. LatLng is possibly outside projection boundary.");
            return false;
        }

        return {
            x: xy[0],
            y: xy[1]
        };

    };

    my.layerPtToLatLng = function (layerPt) {
        var ll = projection.invert([layerPt[0], layerPt[1]]);

        return {
            lng: ll[0],
            lat: ll[1]
        };
    };

    my.getMapScale = function () {
        return mapScale;
    };

    my.setView = function (latLng, zoom) {

    };

    my.hideAllCountyBorders = function () {
        hideAllCountyBorders();
    };

    my.rotateProjection = function (lat, lng, roll) {
        projection.rotate([lat, lng, roll]);
    };

    my.selectState = function (stateFIPS) {
        selectState(stateFIPS);
    };

    my.resetStateFillColor = function () {
        // select the states
        var statePolygons = d3.selectAll(".feature.state");
        statePolygons.transition()
            .duration(500)
            .style("fill", "#ccc")
            .attr("opacity", 1);
    };

    my.zoomToFullExtent = function () {
        zoomToFullExtent();
    };

    // Debug
    my.getPopulationDensityColor = function () {
        return populationDensityColor;
    };

    my.disableTooltip = function () {
        tooltipEnabled = false;
    };

    my.enableTooltip = function () {
        tooltipEnabled = true;
    };

    my.zoomToCircle = function (c) {
        zoomToCircle(c);
    };

    my.zoomToRectangle = function (rect) {
        zoomToRectangle(rect);
    };

    my.setChoroplethAndLegend = function (m) {
        model_copy = m;
        // if this is county flow data, color the counties by pop density
        // TODO this is done on every single iteration of the method.
        // It only needs to be done once.
        if (Flox.isCountyMode() && Flox.getSelectedState() !== false) {
            colorCountiesByPopulationDensity();
        } else {
            // color the states by population density
            colorStatesByPopulationDensity();
        }
    };

    return my;
};

export default  MapComponent_d3